You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Feature: add CivitAI metadata support for LoRA trigger phrase refresh and install metadata enrichment.
This PR adds CivitAI as a metadata source alongside Hugging Face for LoRA installs and Model Manager trigger phrase refresh. CivitAI trainedWords are mapped into existing trigger_phrases, while custom user phrases are preserved. The refresh path stores a concrete CivitAI model-version source URL when metadata is resolved, including cases where CivitAI returns no trained words.
The frontend adds a LoRA-only refresh action in the Model Manager trigger phrase panel. Prompt insertion continues to use the existing Add Prompt Trigger UI.
Follow-up noted separately on the PR: consider adding CivitAI API key management in Settings so users do not need to edit YAML/config files manually.
View Screenshots
Related Issues / Discussions
None linked.
QA Instructions
Validated locally with:
uv run --extra test pytest tests\backend\model_manager\model_metadata\test_civitai_metadata_fetch.py tests\app\routers\test_model_manager.py tests\app\services\model_install\test_model_install.py::test_civitai_download_url_metadata_fetch_failure_falls_back_to_direct_download tests\app\services\model_install\test_model_install.py::test_civitai_download_url_metadata_fetch_returns_metadata_files -q
uv run --extra test pytest tests\app\services\model_install\test_model_install.py -q
uv run --extra test pytest tests\app\routers\test_model_manager.py -q
uv run --extra test ruff check invokeai\backend\model_manager\metadata\fetch\civitai.py invokeai\app\api\routers\model_manager.py invokeai\app\services\model_install\model_install_default.py tests\backend\model_manager\model_metadata\test_civitai_metadata_fetch.py tests\app\routers\test_model_manager.py tests\app\services\model_install\test_model_install.py
pnpm --dir invokeai\frontend\web lint:tsc
Manual QA target:
Open Model Manager.
Select a LoRA model with a CivitAI source or resolvable model hash.
Use the trigger phrase refresh action.
Confirm trigger phrases are added/restored without removing custom phrases.
Confirm Add Prompt Trigger still lists active LoRA trigger phrases.
Merge Plan
No special merge sequencing required. This PR does not include database migrations or model record schema migrations.
Checklist
The PR has a short but descriptive title, suitable for a changelog
Tests added / updated (if applicable)
❗Changes to a redux slice have a corresponding migration
Documentation added / updated (if applicable)
Updated What's New copy (if doing a release after this PR)
Medium:invokeai/app/api/routers/model_manager.py:430 calls _get_civitai_hash and forwards the result to CivitAI's model-versions/by-hash/<hash> endpoint, but the default model-install pipeline computes hashes with the blake3 algorithm (invokeai/backend/model_hash/model_hash.py:14-20,57), and most existing LoRA configs already in the DB carry blake3:<hex> hashes (invokeai/backend/model_manager/configs/base.py:54). The router naively strips the blake3: prefix and queries CivitAI's by-hash lookup, which historically indexes by SHA256/AutoV2 and not by reliable BLAKE3 of the same file bytes that Invoke hashed. For any LoRA that lacks source_url/source of the form https://civitai.com/...?modelVersionId=... and lacks a cached source_api_response, the hash fallback will return 404 and the frontend will surface a confusing "Unable to refresh trigger phrases" error even though the LoRA is on CivitAI. This is the dominant real-world case for already-installed LoRAs predating this feature, and there is no preflight to disable the refresh button or surface a clearer error.
To expose this issue, add a test that calls refresh_model_trigger_phrases against a LoRA whose hash field is "blake3:<value>" and whose source is a HuggingFace repo id, mocks from_hash to raise 404, and asserts that the endpoint returns a user-facing detail explaining that no CivitAI lookup key was usable, instead of the generic "not found" string.
Medium:invokeai/app/api/routers/model_manager.py:421 raises HTTPException(404, ...) whose detail is built from raw English exception strings (e.g. "Unable to resolve CivitAI metadata: ..."), and the frontend at invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/TriggerPhrases.tsx:89 puts error?.data?.detail directly into the toast title without an i18n key. The 400-on-non-LoRA path at invokeai/app/api/routers/model_manager.py:412 is also a hardcoded English string. Localized UIs will permanently show English error copy for this feature, regressing the project's existing pattern of routing user-visible strings through invokeai/frontend/web/public/locales/en.json.
To expose this issue, add a test that asserts the failure-toast title falls back to t('modelManager.triggerPhrasesRefreshFailed') when error.data.detail is present, or move the backend to return an error code that the frontend maps to a translation key.
Medium:invokeai/app/services/model_install/model_install_default.py:761-779 rewrites the previous unconditional except ValueError: pass fallback into a chain where any non-Civitai fetcher (today, only HuggingFace via get_fetcher_from_url) re-raisesValueError/RequestException/UnknownMetadataException. Pre-PR, a ValueError raised from inside the HuggingFace metadata fetch path (e.g. inside metadata.download_urls(session=...) or any sub-call) was swallowed and the installer fell back to a single direct-URL download; post-PR, the same error now propagates and breaks the install. The router-level call metadata = fetcher(**kwargs).from_url(...) is the same call site, so any latent HF code path that throws ValueError is now a regression for HF URL installs.
To expose this issue, add a test that exercises _remote_files_from_source(URLModelSource(url="https://huggingface.co/<repo>")) where the HF fetcher raises ValueError, and assert the previous fallback contract (single direct-URL RemoteModelFile, no metadata, no exception bubbling out) still holds, or update tests to encode the new contract explicitly.
Low:invokeai/app/api/routers/model_manager.py:165-170 (the _get_refreshed_civitai_source_url helper) silently overwrites a stored config.source_url that is a richer slugged CivitAI URL (e.g. https://civitai.com/models/111/some-lora) with the API-normalized https://civitai.com/models/111?modelVersionId=222, losing the human-readable slug. This is asserted by tests/app/routers/test_model_manager.py:299 as the desired behavior, but it mutates user-visible metadata on every refresh and there is no user-facing notice. If a user manually edited source_url for organizational reasons, they will not be told it was rewritten.
To expose this issue, add a test that pins the expectation that refresh never replaces a user-set slugged CivitAI URL with a slugless canonical variant unless the user opts in, then update the router to keep config.source_url whenever it already matches the resolved model_id/model_version_id.
Low:invokeai/backend/model_manager/util/lora_metadata_extractor.py:145 changes if trigger_phrases: ... to if trigger_phrases and not info.trigger_phrases:. This silently changes installer behavior for all LoRA installs, not just CivitAI ones: prior to this PR, embedded LoRA file metadata always overwrote info.trigger_phrases; now it only fills if empty. A user reinstalling a LoRA whose embedded trainingset_metadata was updated by the trainer will no longer pick up the corrected list, because the existing (potentially stale) phrases are preserved instead. The PR description scopes itself to CivitAI refresh but this behavior change leaks into the normal install path.
To expose this issue, add a test that reinstalls a LoRA whose stored config has trigger_phrases={"old"} and whose file embedded metadata advertises ["new"], and assert the expected merge semantics. The current PR's tests do not pin this case.
Low:invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/TriggerPhrases.tsx:118-125 shows the Refresh button for every LoRA regardless of whether it has a CivitAI source or a CivitAI-usable hash. The most common real failure (Medium finding above) is therefore a one-click action that always 404s for non-CivitAI LoRAs and locally-trained LoRAs, with no preflight gate. The button's disabled/loading state is also coupled to isRefreshing only, so a click during isLoading (an in-flight updateModel) would still fire a refresh in parallel because the button's own isLoading prop does not reflect isLoading from useUpdateModelMutation.
To expose this issue automatically, add a logic-level test around the button enablement selector (extracting canRefreshTriggerPhrases and the disabled condition into a unit-testable helper) and assert it accounts for both mutations. There is no approved DOM testing framework at present, so otherwise call this out for manual verification.
Low:invokeai/backend/model_manager/metadata/fetch/civitai.py:151 raises response.raise_for_status() for any non-404 HTTP error and lets that bubble. CivitAI is documented to rate-limit unauthenticated requests; a 429 from CivitAI will surface as a requests.HTTPError that the router does not catch (router only catches UnknownMetadataException), so the response will be a 500 from FastAPI's default handler instead of the documented 404/400. The frontend toast then shows no detail.
To expose this issue, add a test that mounts a TestAdapter returning status 429 on the model-versions/{id} URL and asserts the route returns a 4xx with a user-facing detail, not a 500.
Low:invokeai/backend/model_manager/metadata/fetch/civitai.py:204-208 parses sizeKB via int(float(size) * 1024). CivitAI returns sizeKB as a float (e.g. 2.5), so values like 2.5 * 1024 = 2560.0 are fine, but for files whose listed sizeKB is non-numeric (string, null in nested form), the float(size) call raises ValueError/TypeError. The fetcher does not catch this, so a malformed CivitAI response yields a TypeError (not caught by the router's except (UnknownMetadataException, requests.RequestException, ValueError)) and propagates as a 500.
To expose this issue, add a test that returns a CivitAI version response whose primary file has "sizeKB": null and assert the fetcher raises UnknownMetadataException (not TypeError).
Open Questions
Does the prefix-stripping in _get_civitai_hash actually match a hash CivitAI indexes? CivitAI's by-hash endpoint accepts SHA256/AutoV1/AutoV2/CRC32/BLAKE3, but it is unclear whether BLAKE3 lookups exist for the majority of historical uploads. If CivitAI does not store BLAKE3 hashes for most files, the hash fallback is effectively dead for the typical existing user. This is the basis of the Medium finding above; without network access I cannot confirm coverage.
_get_civitai_hash returns config.hash.partition(":")[2] or config.hash and discards algorithm metadata. Should this gate the call by checking config.hash.startswith("blake3:") and skip the hash lookup entirely when the algorithm is not one CivitAI indexes? The current code silently sends a likely-useless hash and burdens CivitAI's API.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Feature: add CivitAI metadata support for LoRA trigger phrase refresh and install metadata enrichment.
This PR adds CivitAI as a metadata source alongside Hugging Face for LoRA installs and Model Manager trigger phrase refresh. CivitAI
trainedWordsare mapped into existingtrigger_phrases, while custom user phrases are preserved. The refresh path stores a concrete CivitAI model-version source URL when metadata is resolved, including cases where CivitAI returns no trained words.The frontend adds a LoRA-only refresh action in the Model Manager trigger phrase panel. Prompt insertion continues to use the existing Add Prompt Trigger UI.
Follow-up noted separately on the PR: consider adding CivitAI API key management in Settings so users do not need to edit YAML/config files manually.
View Screenshots
Related Issues / Discussions
None linked.
QA Instructions
Validated locally with:
uv run --extra test pytest tests\backend\model_manager\model_metadata\test_civitai_metadata_fetch.py tests\app\routers\test_model_manager.py tests\app\services\model_install\test_model_install.py::test_civitai_download_url_metadata_fetch_failure_falls_back_to_direct_download tests\app\services\model_install\test_model_install.py::test_civitai_download_url_metadata_fetch_returns_metadata_files -quv run --extra test pytest tests\app\services\model_install\test_model_install.py -quv run --extra test pytest tests\app\routers\test_model_manager.py -quv run --extra test ruff check invokeai\backend\model_manager\metadata\fetch\civitai.py invokeai\app\api\routers\model_manager.py invokeai\app\services\model_install\model_install_default.py tests\backend\model_manager\model_metadata\test_civitai_metadata_fetch.py tests\app\routers\test_model_manager.py tests\app\services\model_install\test_model_install.pypnpm --dir invokeai\frontend\web lint:tscManual QA target:
Merge Plan
No special merge sequencing required. This PR does not include database migrations or model record schema migrations.
Checklist
What's Newcopy (if doing a release after this PR)